import cmd
import sys
import shlex
import os
import socket
import getpass
import itertools
import signal
import stat
import pwd
import grp
import time

class PyShell(cmd.Cmd):
    def __init__(self):
        super().__init__()
        self.jobs = []
        self.prompt = f'{getpass.getuser()}@{socket.gethostname()}:{PyShell.full_to_home_abbr(os.getcwd())}$ '

    def do_exit(self, arg_str : str) -> None:
        args = shlex.split(arg_str)
        len_args = len(args)

        if len_args > 1 :
            print('too many arguments', file = sys.stderr)
            return
        if len_args == 0 or args[0] == ' ':
            exit(0)
        try:
            exit(int(args[0]))
        except ValueError:
            print('exit: invalid exit code', file = sys.stderr)
    
    def do_pwd(self, args_placeholder : str) -> None:
        return print(os.environ['PWD'])

    def do_ls(self, args_str : str) -> None:
        tokens = shlex.split(args_str) if args_str else []
        options = []
        targets = []
        allowed_options = {'a', 'A', 'l'}
        for token in tokens:
            if token != '-' and token.startswith('-') and not targets:
                options.extend(token[1:])
            else:
                targets.append(token)

        for op in options:
            if op not in allowed_options:
                return print(f"ls: invalid options -- '{op}'")

        if not targets:
            targets = ['.']

        option_set = set(options)
        show_hidden = ('a' in option_set) or ('A' in option_set)
        include_dot_entries = 'a' in option_set
        long_format = 'l' in option_set

        for idx, target in enumerate(targets):
            if not target:
                target = '.'
            path = PyShell.home_abbr_to_full(target)
            try:
                if os.path.isdir(path):
                    if len(targets) > 1:
                        print(f'{target}:')
                    entries = PyShell._collect_ls_entries(path, show_hidden, include_dot_entries)
                    for entry in entries:
                        if long_format:
                            print(PyShell._format_entry_long(path, entry))
                        else:
                            print(entry)
                    if len(targets) > 1 and idx != len(targets) - 1:
                        print()
                elif os.path.exists(path):
                    if long_format:
                        base_dir = os.path.dirname(path) or '.'
                        entry_name = os.path.basename(path) or target
                        print(PyShell._format_entry_long(base_dir, entry_name))
                    else:
                        print(target)
                else:
                    raise FileNotFoundError
            except FileNotFoundError:
                print(f'ls: cannot access {target}: No such file or directory', file=sys.stderr)
            except PermissionError:
                print(f'ls: cannot open {target}: Permission denied', file=sys.stderr)

    @staticmethod
    def _collect_ls_entries(path : str, show_hidden : bool, include_dots : bool) -> [str]:
        entries = []
        # 参数里是否有a
        if include_dots:
            entries.extend(['.', '..'])
        for entry in sorted(os.listdir(path)):
            # 是否不存在a和A并且以.作为前缀
            if not show_hidden and entry.startswith('.'):
                continue
            entries.append(entry)
        return entries

    @staticmethod
    def _format_entry_long(base_dir : str, entry : str) -> str:
        full_path = os.path.join(base_dir, entry)
        try:
            stats = os.lstat(full_path)
        except OSError:
            return entry

        link_target = None
        if stat.S_ISLNK(stats.st_mode):
            try:
                link_target = os.readlink(full_path)
            except OSError:
                link_target = None

        mode = stat.filemode(stats.st_mode)
        nlink = stats.st_nlink

        try:
            owner = pwd.getpwuid(stats.st_uid).pw_name
        except KeyError:
            owner = str(stats.st_uid)
        try:
            group = grp.getgrgid(stats.st_gid).gr_name
        except KeyError:
            group = str(stats.st_gid)

        size = stats.st_size
        name_filed = entry
        if link_target is not None:
            name_filed = f"{entry} -> {link_target}"
        mtime = time.strftime("%b %d %H:%M", time.localtime(stats.st_mtime))
        return f"{mode} {nlink:>2} {owner} {group} {size:>6} {mtime} {name_filed}"


    @staticmethod
    def home_abbr_to_full(abbr_path : str) -> str:
        if abbr_path.startswith('-') and 'OLDPWD' in os.environ :
            abbr_path = abbr_path.replace('-', os.environ['OLDPWD'], 1)
        elif abbr_path.startswith('~'):
            abbr_path = abbr_path.replace('~', os.environ['HOME'], 1)
        return abbr_path
    
    @staticmethod
    def full_to_home_abbr(full_path : str) -> str:
        if full_path.startswith('-') and 'OLDPWD' in os.environ :
            full_path = full_path.replace(os.environ['OLDPWD'], '-', 1)
        elif full_path.startswith('~'):
            full_path = full_path.replace(os.environ['HOME'], '~', 1)
        return full_path
    
    def do_cd(self, arg_str : str) -> None:
        args = shlex.split(arg_str)
        len_args = len(args)

        if len_args > 1:
            print("too many arguments: ", file = sys.stderr)
            return
        try:
            if len == 0 or args[0] == '':
                os.chdir(os.environ['HOME'])
            else:
                os.chdir(PyShell.home_abbr_to_full(args[0]))
        except FileNotFoundError:
            print("cd : invalid path", file = sys.stderr)
        except NotADirectoryError:
            print("cd : not a dircetory", file = sys.stderr)
        else:
            # 在成功切换目录后，用 os.getcwd() 获取新路径并更新 self.prompt
            self.prompt = f'{getpass.getuser()}@{socket.gethostname()}:{PyShell.full_to_home_abbr(os.getcwd())}$ '

    def default(self, line : str) -> None:
        commands = shlex.split(line)
        if not commands:
            return
        segments = [list(command) for is_pipe_operator, command in itertools.groupby(commands, lambda word: word == '|') if not is_pipe_operator]
        self.main_function(segments)

    def main_function(self, args_list : [[str]], is_foreground = True) -> None:
        children_pids = []
        old_fds = None

        def _clean_up(error : OSError) -> None:
            for _pid in children_pids:
                try:
                    os.kill(_pid, signal.SIGKILL)
                except ProcessLookupError:
                    pass
            if args_list:
                print(f'{args_list[0][0]}: {error}', file=sys.stderr)

        pid = -1
        try:
            for i, cmd_args in enumerate(args_list):
                next_pipe = os.pipe() if i < len(args_list) - 1 else (None, None)
                pid = os.fork()

                if pid == 0:
                    redirect, cmd_args = PyShell.redirect_func(cmd_args)

                    if old_fds:
                        os.dup2(old_fds[0], sys.stdin.fileno())
                        os.close(old_fds[0])
                        os.close(old_fds[1])
                        if redirect[sys.stdin.fileno()]:
                            raise OSError('invalid usage redirection and (or) piping')

                    if next_pipe[0] is not None:
                        os.close(next_pipe[0])
                        os.dup2(next_pipe[1], sys.stdout.fileno())
                        os.close(next_pipe[1])
                        if redirect[sys.stdout.fileno()]:
                            raise OSError('invalid usage redirection and (or) piping')

                    os.execvp(cmd_args[0], cmd_args)

                children_pids.append(pid)
                if old_fds:
                    os.close(old_fds[0])
                    os.close(old_fds[1])
                old_fds = next_pipe if next_pipe[0] is not None else None

            self.jobs.append(('fg', children_pids))
            try:
                for child_pid in children_pids:
                    os.waitpid(child_pid, 0)
                self.jobs.pop()
            except ChildProcessError:
                pass
        except OSError as e:
            _clean_up(e)
            if pid == 0:
                exit(1)
            else:
                return

    @staticmethod
    def redirect_func(args_list : [str]) -> ((bool, bool, bool), [str]):
        args_list = list(args_list)
        is_redirected = [False, False, False]

        if '<' in args_list:
            if not is_redirected[sys.stdin.fileno()]:
                is_redirected[sys.stdin.fileno()] = True
                idx = args_list.index('<')
                filepath = args_list[idx + 1]

                if idx + 1 < len(args_list):
                    args_list.pop(idx + 1) # 先移除文件名
                    args_list.pop(idx)     # 再移除 '<'
                else:
                    raise OSError('invalid usage of redirection and (or) piping3')

                fd = os.open(filepath, os.O_RDONLY, 0o644)
                os.dup2(fd, sys.stdin.fileno())
                os.close(fd)
            else:
                raise OSError('invalid usage of redirection and (or) piping4')
            
        if '>' in args_list:
            if not is_redirected[sys.stdout.fileno()]:
                is_redirected[sys.stdout.fileno()] = True
                idx = args_list.index('>')
                filepath = args_list[idx + 1]
                if idx + 1 < len(args_list):
                    args_list.pop(idx + 1)
                    args_list.pop(idx)
                else:
                    raise OSError('invalid usage of redirection and (or) piping5')
                fd = os.open(filepath, os.O_WRONLY | os.O_TRUNC | os.O_CREAT, 0o644)
                os.dup2(fd, sys.stdout.fileno())
                os.close(fd)    
            else:
                raise OSError('invalid usage of redirection and (or) piping6')

        
        if '>>' in args_list:
            if not is_redirected[sys.stdout.fileno()]:
                is_redirected[sys.stdout.fileno()] = True
                idx = args_list.index('>>')
                filepath = args_list[idx + 1]
                if idx + 1 < len(args_list):
                    args_list.pop(idx + 1)
                    args_list.pop(idx)
                else:
                    raise OSError('invalid usage of redirection and (or) piping7')   
                
                fd = os.open(filepath, os.O_APPEND | os.O_CREAT | os.O_WRONLY, 0o644)
                os.dup2(fd, sys.stdout.fileno())
                os.close(fd)
            else:
                raise OSError('invalid usage of redirection and (or) piping8')


        if '2>' in args_list:
            if not is_redirected[sys.stderr.fileno()]:
                is_redirected[sys.stderr.fileno()] = True
                idx = args_list.index('2>')
                filepath = args_list[idx + 1]

                if idx + 1 < len(args_list):
                    args_list.pop(idx + 1)
                    args_list.pop(idx)
                else:
                    raise OSError('invalid usage of redirection and (or) piping9')
                fd = os.open(filepath, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o644)
                os.dup2(fd, sys.stderr.fileno())
                os.close(fd)
            else:
                raise OSError('invalid usage of redirection and (or) piping10')

        if '2>>' in args_list:
            if not is_redirected[sys.stderr.fileno()]:
                is_redirected[sys.stderr.fileno()] = True
                idx = args_list.index('2>>')
                filepath = args_list[idx + 1]

                if idx + 1 < len(args_list):
                    args_list.pop(idx + 1)
                    args_list.pop(idx)
                else:
                    raise OSError('invalid usage of redirection and (or) piping11')
               
                fd = os.open(filepath, os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o644)
                os.dup2(fd, sys.stderr.fileno())
                os.close(fd)
            else:
                raise OSError('invalid usage of redirection and (or) piping12')
            

        if '&>' in args_list:
            if not is_redirected[sys.stderr.fileno()] and not is_redirected[sys.stdout.fileno()]:
                is_redirected[sys.stderr.fileno()] = True
                is_redirected[sys.stdout.fileno()] = True
                idx = args_list.index('&>')
                filepath = args_list[idx + 1]

                if idx + 1 < len(args_list):
                    args_list.pop(idx + 1)
                    args_list.pop(idx)
                else:
                    raise OSError('invalid usage of redirection and (or) piping13')
                
                fd = os.open(filepath, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o644)
                os.dup2(fd, sys.stderr.fileno())
                os.dup2(fd, sys.stdout.fileno())
                os.close(fd)
            else:
                raise OSError('invalid usage of redirection and (or) piping14')            
        return tuple(is_redirected), args_list
    
if __name__ == '__main__' :
    PyShell().cmdloop()